// Copyright 2012 Jay Young. All Rights Reserved.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
//      http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS-IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.

/**
 * @fileoverview
 *
 * A Documenter visitor that documents both class and interface constructors.
 */

var strings = require('ringo/utils/strings'),
    Token = com.google.javascript.rhino.Token,
    NodeUtil = com.google.javascript.jscomp.NodeUtil;



/**
 * A node visitor that documents both class and interface constructors.
 *
 * @constructor
 * @implements {Documenter.Visitor}
 */
var ConstructorV = exports.ConstructorV = function() {
  /**
   * The next visitor in the chain.
   *
   * @type {Documenter.Visitor}
   * @private
   */
  this.nextVisitor_ = null;
};


/**
 * @param {Documenter.Visitor} visitor The next visitor in the chain.
 */
ConstructorV.prototype.setNext = function(visitor) {
  this.nextVisitor_ = visitor;
};


/**
 * Inspect a node to see if it's a class or interface constructor.  If it is,
 * add the appropriate data to the docInfo object.
 *
 * @param {Node} node The node to visit.
 * @parma {DocInfo} docInfo The page's documentation info object.
 * @param {JSDocInfo} jsdoc The jsdoc for the given node or null if no
 *    doc comments were attached.
 */
ConstructorV.prototype.visit = function(node, docInfo, jsdoc) {
  if (jsdoc &&
     ((jsdoc.isConstructor() && !isLocal(node)) ||
      jsdoc.isInterface())) {

    var funcProp, funcName, className;
    
    if (node.getType() === Token.ASSIGN) {
      // Handles the conventional namespaced declaractions:
      //   namespace.Constructor = function() {...};

      var getProp = node.getFirstChild();
      funcProp = getProp.getNext();
      
      if (funcProp.getType() === Token.FUNCTION) {
        funcName = funcProp.getFirstChild().getQualifiedName();
      }
      else {
        funcName = '';
      }
      
      className = getProp.getQualifiedName();
    }
    else if (node.getType() === Token.VAR) {
      // Handles the CommonJS style exports declarations:
      //   var Constructor = exports.Constructor = function() {...};

      var nameProp = node.getFirstChild();
      className = funcName = nameProp.getQualifiedName();
      funcProp = nameProp.getFirstChild().getChildAtIndex(1);
      funcName = funcProp.getFirstChild().getQualifiedName();
    }
    else if (node.getType() === Token.FUNCTION) {
      // Handles the function expression:
      //   function Constructor() {...}

      funcProp = node;
      funcName = className = funcProp.getFirstChild().getQualifiedName();
    }

    var classDesc,
        isInterface = false;
    if (jsdoc.isConstructor()) {
      classDesc = docInfo.getClass(className);
    }
    else {
      isInterface = true;
      classDesc = docInfo.getInterface(className);
    }

    if (funcName !== '' && (! strings.endsWith(className, funcName))) {
      print('WARNING: function name "' + funcName + '" does not match ' +
            'property name "' + className +
            '" on line ' + funcProp.getLineno() + '.');
    }

    classDesc.name = docInfo.expandAlias(className);
    classDesc.classDoc = jsdoc.getBlockDescription() || '';
    classDesc.visibility = docInfo.visibility(jsdoc.getVisibility()) || '';
    
    if (jsdoc.isDeprecated()) {
      classDesc.deprecationReason = jsdoc.getDeprecationReason() ||
          'No reason given.';
    }

    // Capture @see annotations
    var references = jsdoc.getReferences();
    if (references) {
      var refList = classDesc.references = [],
          iterator = references.iterator();
      while (iterator.hasNext()) {
        refList.push(iterator.next());
      }
    }


    if (! isInterface) {
      classDesc.baseType = docInfo.resolveType(jsdoc.getBaseType());
    }
    else {
      var extendedInterfaces = jsdoc.getExtendedInterfaces();
      if (extendedInterfaces.size() > 0) {
        var interfaces = classDesc.extendedInterfaces = [],
            iter = extendedInterfaces.iterator();
        while (iter.hasNext()) {
          interfaces.push(docInfo.resolveType(iter.next()));
        }
      }
    }

    var implemented = classDesc.interfaces = [];
    var interfaces = jsdoc.getImplementedInterfaces().iterator();
    while (interfaces.hasNext()) {
      implemented.push(docInfo.resolveType(interfaces.next()));
    }

    var paramCount = jsdoc.getParameterCount();
    if (paramCount > 0) {
      var params = jsdoc.getParameterNames().iterator(),
          paramDescs = classDesc.params = [];

      while (params.hasNext()) {
        var param = params.next();
        paramDescs.push({
          name: param,
          type: docInfo.resolveType(jsdoc.getParameterType(param)),
          desc: jsdoc.getDescriptionForParameter(param)
        });
      }
    }
  }

  if (this.nextVisitor_) {
    this.nextVisitor_.visit(node, docInfo, jsdoc);
  }
};


/**
 * Determine whether a constructor function is assigned to a local variable
 * inside a function scope AND isn't being assigned to a namespace.  In this
 * situation, the constructor is not avaailable outside the function and is most
 * likely being used as a temporary constructor.
 *
 * This check is a direct result of the use of "tempCtor" in goog.base.  It will
 * certainly have to be adjusted to accomodate other coding conventions.
 *
 * @param {Node} node The constructor's node.
 * @return {boolean} Whether this constructor declaration is trapped in a
 *    local scope.
 */
function isLocal(node) {
  var isLocal = true;

  var funcName;
  if (node.getType() === Token.ASSIGN &&
      node.getChildAtIndex(1).getType() === Token.FUNCTION) {
    funcName = NodeUtil.getNearestFunctionName(node.getChildAtIndex(1));
  }
  else if (node.getType() === Token.ASSIGN) {
    // Something other than a function is being assigned to this object
    // For example:  goog.Delay = goog.async.Delay;
    var funcName = node.getFirstChild().getQualifiedName();
  }
  else if (node.getType() === Token.FUNCTION) {
    funcName = NodeUtil.getNearestFunctionName(node);
  }
  else if (node.getType() === Token.VAR) {
    funcName = node.getFirstChild().getQualifiedName();
  }
  
  // If the constructor is being assigned to an object, assume it's being
  // assigned to a global namespace and is therefore available outside of the
  // local scope.  This may need to be refined later.
  if (funcName && funcName.indexOf('.') !== -1) {
    isLocal = false;
  }
  
  // If the constructor is being assigned in the global scope, it's not local.
  if (node.getParent().getType() === Token.SCRIPT) {
    isLocal = false;
  }

  return isLocal;
}